package com.heima.service.impl;


import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.dfa.WordTree;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.aliyun.GreenImageScan;
import com.heima.aliyun.GreenTextScan;
import com.heima.apis.ArticleApi;
import com.heima.mapper.WmChannelMapper;
import com.heima.mapper.WmNewsMapper;
import com.heima.mapper.WmSensitiveMapper;
import com.heima.minio.MinIoTemplate;
import com.heima.model.article.dtos.ArticleDto;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.vos.Result;
import com.heima.model.wemedia.dtos.WmNewsDto;
import com.heima.model.wemedia.pojos.WmChannel;
import com.heima.model.wemedia.pojos.WmNews;
import com.heima.model.wemedia.pojos.WmSensitive;
import com.heima.ocr.Tess4jClient;
import com.heima.service.WmNewsService;
import lombok.extern.slf4j.Slf4j;
import net.sourceforge.tess4j.TesseractException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
@Transactional
public class WmNewsServiceImpl extends ServiceImpl<WmNewsMapper, WmNews> implements WmNewsService {

    @Autowired
    private WmNewsMapper wmNewsMapper;

    @Autowired
    private WmChannelMapper wmChannelMapper;

    @Autowired
    private GreenTextScan greenTextScan;

    @Autowired
    private GreenImageScan greenImageScan;

    @Autowired
    private ArticleApi articleApi;

    @Autowired
    private MinIoTemplate minIoTemplate;

    private String list2String(List<String> images) {
        if (CollectionUtil.isEmpty(images)) {
            return "";
        }

        return StringUtils.join(images, ",");
    }

    /**
     * ① 发布新闻
     *
     * @param dto 新闻内容
     * @return 成功/失败
     */
    @Override
    public Boolean submitNews(WmNewsDto dto) throws Exception {
        // 1.入参判空
        if (dto == null) {
            throw new RuntimeException("入参不能为空");
        }

        // 2.组装字段 dto -> pojo
        WmNews wmNews = new WmNews();

        // 2.1 拷贝相同的字段
        BeanUtils.copyProperties(dto, wmNews);

        // 2.2 处理数据库中有的，但是入参没有的字段
        //TODO 鉴权还没做，所以先用0
        wmNews.setUserId(0);
        wmNews.setCreatedTime(new Date());
        wmNews.setSubmitedTime(new Date());

        // 2.3 处理数据类型不同的字段

        // 把集合类型的images转成字符串类型
        wmNews.setImages(list2String(dto.getImages()));

        // 判断：如type == -1
        Short type = dto.getType();

        List<String> imageList = new ArrayList<>();
        // 布局方式为：自动
        if (type == -1) {

            // 1. 提取content中所有的图片
            String content = dto.getContent();
            // 将JSON字符串转成Java集合
            List<Map> contentList = JSON.parseArray(content, Map.class);
            if (!CollectionUtil.isEmpty(contentList)) {
                // 遍历集合
                for (Map map : contentList) {
                    if (map == null) {
                        continue;
                    }

                    // 提取出里面 type = image 的这一行，获取value的值
                    Object type1 = map.get("type");
                    if ("image".equals(type1)) {
                        String value = (String) map.get("value");
                        imageList.add(value);
                    }
                }
            }

            // 2. 根据图片数量决定type字段取值
            int size = imageList.size();
            // size = 0 => type => 无图(0)
            // size = 1 => type => 单图(1)
            // size => 多图模式(3)
            if (size > 1) {
                wmNews.setType((short) 3);
            } else {
                wmNews.setType((short) size);
            }

            // 3. 提取出来的图片列表，覆盖到数据库中的images字段
            wmNews.setImages(list2String(imageList.subList(0, size < 3 ? size : 3)));
        }

        Integer newsId = dto.getId();
        // 3.id为空 -> 新增
        if (newsId == null) {
            // 新增数据的时候，需要手动创建id
            wmNews.setEnable((short) 1);
            wmNews.setReason("");
            wmNews.setArticleId(0L);
            int insertResult = wmNewsMapper.insert(wmNews);
            if (insertResult != 1) {
                throw new RuntimeException("新增新闻错误");
            }
        }

        // 4.id不为空 -> 修改
        else {
            int updateResult = wmNewsMapper.updateById(wmNews);
            if (updateResult != 1) {
                throw new RuntimeException("修改新闻错误");
            }
        }

        // TODO 要改成调用审核方法
        autoScan(wmNews);

        return true;
    }

    /**
     * 检查那些未发布状态，并且发布时间已到的新闻（提取一个）
     *
     * @return WmNews
     */
    public WmNews check() {
        Page<WmNews> page = new Page<>(1, 1);

        LambdaQueryWrapper<WmNews> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(WmNews::getStatus, 1);
        wrapper.le(WmNews::getPublishTime, new Date());

        Page<WmNews> wmNews = wmNewsMapper.selectPage(page, wrapper);
        if (wmNews == null || CollectionUtil.isEmpty(wmNews.getRecords())) {
            return null;
        }

        return wmNews.getRecords().get(0);
    }

    /**
     * ③ 向文章服务同步数据用的（这步执行完后，理论上来说APP就可以看到新闻了）
     *
     * @param wmNews 自媒体新闻内容
     */
    public void refreshArticle(WmNews wmNews) {
        // feign接口调用完后，要返回articleId（app端的文章id）
        // 然后把articleId回写到当前的新闻表（articleId字段中）
        ArticleDto articleDto = new ArticleDto();
        BeanUtils.copyProperties(wmNews, articleDto);

        // 给article主键赋值，后续article处理的时候，才能判断是新增还是修改操作
        articleDto.setId(wmNews.getArticleId());

        // 作者
        // TODO 鉴权有了之后，直接用用户id去查用户信息就可以了

        // 频道
        WmChannel wmChannel = wmChannelMapper.selectById(wmNews.getChannelId());
        if (wmChannel != null) {
            articleDto.setChannelName(wmChannel.getName());
        }

        // 布局
        // wmNews.type 0（无图）,1（单图）,3（多图）
        // article.layout 0（无图）,1（单图）,2（多图）
        articleDto.setLayout(wmNews.getType() == 3 ? 2 : wmNews.getType());

        Result result = articleApi.saveArticle(articleDto);

        // 需要从result中获得创建好的articleId
        if (!result.getCode().equals(200)) {
            throw new RuntimeException("保存文章失败");
        }

        String resultData = JSON.toJSONString(result.getData());

        ApArticle apArticle = JSON.parseObject(resultData, ApArticle.class);

        Long articleId = apArticle.getId();

        // 将articleId回写回wmNews表
        wmNews.setArticleId(articleId);
        wmNews.setStatus((short) 9);
        int update = wmNewsMapper.updateById(wmNews);
        if (update != 1) {
            throw new RuntimeException("回写新闻数据异常");
        }
    }

    /**
     * 提取新闻中的所有文字内容
     *
     * @param wmNews 新闻
     * @return 文字内容
     */
    private String getText(WmNews wmNews) {
        if (wmNews == null) {
            return "";
        }

        String title = wmNews.getTitle();
        String labels = wmNews.getLabels();
        String content = wmNews.getContent();

        // 最终把三个部分的字符串提取出来，组装在一起
        StringBuilder result = new StringBuilder(title + "|" + labels);

        // 想办法从content中提取文字内容

        // 1. 将json转成可以解析的ArrayList
        List<Map> list = JSON.parseArray(content, Map.class);
        if (CollectionUtil.isEmpty(list)) {
            return result.toString();
        }

        // 2. 遍历ArrayList
        for (Map map : list) {
            if (map == null) {
                continue;
            }

            // 3. 判断遍历出来的数据type是不是text，如果是就提取value，组装到Result上面
            Object type = map.get("type");
            if ("text".equals(type)) {
                Object value = map.get("value");

                result.append(value);
            }
        }

        return result.toString();
    }

    /**
     * 解析阿里云审核结果，并且做出对应处理
     *
     * @param result 阿里云审核结果
     * @param wmNews 新闻
     * @return 通过/不通过
     */
    private boolean resultHandler(Map result, WmNews wmNews) {
        // 根据阿里云处理结果，决定要做的事情
        Object suggestion = result.get("suggestion");

        // 通过，就什么也不做
        if ("pass".equals(suggestion)) {
            return true;
        }

        // 不通过，修改状态为2，并且reason填一下
        if ("block".equals(suggestion)) {

            // 修改状态为2
            wmNews.setStatus((short) 2);
            wmNews.setReason("阿里云审核不通过");
            wmNewsMapper.updateById(wmNews);

            return false;
        }

        wmNews.setStatus((short) 3);
        wmNews.setReason("阿里云说它不确定");
        wmNewsMapper.updateById(wmNews);

        // 不确定，修改状态为3
        return false;
    }

    /**
     * 提取新闻中的图片内容
     *
     * @param wmNews 新闻
     * @return 图片内容集合（bytes[]）
     */
    private List<byte[]> getImages(WmNews wmNews) {
        // 1. 入参判空
        if (wmNews == null) {
            return new ArrayList<>();
        }

        // 2. 提取wmNews images封面图 + content里面图片
        String images = wmNews.getImages();
        String content = wmNews.getContent();

        // 3. 字符串类型的images转成集合类型
        List<String> imageList = new ArrayList<>();

        // 4. 封面图处理
        String[] split = images.split(",");
        imageList.addAll(Arrays.asList(split));

        // 5. 内容图处理
        List<Map> list = JSON.parseArray(content, Map.class);
        if (!CollectionUtil.isEmpty(list)) {

            for (Map map : list) {
                if (map == null) {
                    continue;
                }

                Object type = map.get("type");
                if ("image".equals(type)) {
                    Object value = map.get("value");
                    imageList.add((String) value);
                }
            }
        }

        List<byte[]> byteList = new ArrayList<>();
        for (String url : imageList) {
            if (StringUtils.isEmpty(url)) {
                continue;
            }

            byte[] bytes = minIoTemplate.downLoadFile(url);
            byteList.add(bytes);
        }

        return byteList;
    }

    @Autowired
    private Tess4jClient tess4jClient;

    @Autowired
    private WmSensitiveMapper wmSensitiveMapper;

    private String ocrCheck(byte[] bytes) throws IOException, TesseractException {
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        BufferedImage read = ImageIO.read(bais);
        return tess4jClient.doOCR(read);
    }

    /**
     * ② 自动审核新闻内容
     *
     * @param wmNews 新闻
     * @throws Exception 异常
     */
    @Override
    public void autoScan(WmNews wmNews) throws Exception {
        // 1. 入参判空
        if (wmNews == null) {
            throw new RuntimeException("入参不能为空");
        }

        // 2. 提取新闻中的文字内容（content、title、tags）
        String text = getText(wmNews);

        // 3. 提取新闻中的图片内容
        List<byte[]> images = getImages(wmNews);

        // 将已得到的图片内容，去调用ocr方法，从而获得图片中的文字内容
        for (byte[] image : images) {
            // 然后将文字内容和，前面提取到的文字内容组合在一起
            text += "|" + ocrCheck(image);
        }

        // 敏感词检测（正确的做法应该是，在数据库里面建一张敏感词表，然后再这里动态查询出来，生成敏感词树）
        WordTree wordTree = new WordTree();
        // 从数据里里面，把敏感词全部查出来
        List<WmSensitive> wmSensitives = wmSensitiveMapper.selectList(null);
        // 敏感词不为空的时候，才有必要去执行敏感词校验
        if (!CollectionUtil.isEmpty(wmSensitives)) {
            for (WmSensitive wmSensitive : wmSensitives) {
                if (wmSensitive == null) {
                    continue;
                }
                // 把所有的敏感词初始化到 DFA算法的树里面
                wordTree.addWord(wmSensitive.getSensitives());
            }

            List<String> list = wordTree.matchAll(text);
            if (!CollectionUtil.isEmpty(list)) {
                System.out.println("包含违禁词");

                // 修改状态为2
                wmNews.setStatus((short) 2);
                wmNews.setReason("敏感词库审核不通过");
                wmNewsMapper.updateById(wmNews);

                return;
            }
        }

        // 3. 调用阿里云文字审核
        Map textScan = greenTextScan.greeTextScan(text);

        // 4. 判断阿里云审核结果，对应做解决方案
        Boolean textScanResult = resultHandler(textScan, wmNews);
        if (!textScanResult) {
            return;
        }

        // 6. 调用阿里云图片审核
        Map imageScan = greenImageScan.imageScan(images);

        // 7. 检测结果，对应解决方案
        Boolean imageScanResult = resultHandler(imageScan, wmNews);
        if (!imageScanResult) {
            return;
        }

        // 8. 调用feign的保存接口，实现向文章服务同步新闻的功能
        // 5.判断发布时间，决定是否立即生成app端文章
        Date publishTime = wmNews.getPublishTime();
        Date currentTime = new Date();

        // 如果发布时间 小于等于当前时间，那么就立即调用app端 feign接口，同步文章
        // 如果发布时间 大于当前时间，就不管它（交给定时任务去处理）
        if (!publishTime.after(currentTime)) {
            refreshArticle(wmNews);
        }

    }
}
